Skip to main content

Go 并发编程测试分析

· 7 min read
Softwore Developer

Go 并发编程测试分析

本文是对https://colobu.com/2019/04/28/go-concurrency-quizzes/晁岳攀老师博客中提到的并发测试例子的讲解。

我按问题类型来分类说明错误原因。

所以P中没有可以调度的G时就会出现死锁

这个问题涉及到的题目为:1、Mutex2、RWMutex.

1、Mutex

package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
var chain string
func main() {
chain = "main"
A()
fmt.Println(chain)
}
func A() {
mu.Lock()
defer mu.Unlock()
chain = chain + " --> A"
B()
}
func B() {
chain = chain + " --> B"
C()
}
func C() {
mu.Lock()
defer mu.Unlock()
chain = chain + " --> C"
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!

这一题的问题是runtime中只有一个G,但是当运行到A()方法是以及加锁,后面的C()方法接着去加锁,就会出现拿不到锁,于是这个G的状态就变为不可运行,所以就出现了deadlock!.

2、RWMutex

package main
import (
"fmt"
"sync"
"time"
)
var mu sync.RWMutex
var count int
func main() {
go A()
time.Sleep(2 * time.Second)
mu.Lock()
defer mu.Unlock()
count++
fmt.Println(count)
}
func A() {
mu.RLock()
defer mu.RUnlock()
B()
}
func B() {
time.Sleep(5 * time.Second)
C()
}
func C() {
mu.RLock()
defer mu.RUnlock()
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!

这一题的原因也是一样的,GA休眠之后状态就会变为等待,此时,主G去那锁也没有那到,就会变为不可运行状态,并让出cpu,此时所有的G都不可运行就出现死锁了。

WaitGroup 使用问题

package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(time.Millisecond)
wg.Done()
wg.Add(1)
}()
wg.Wait()
}
$ go run main.go
panic: sync: WaitGroup is reused before previous Wait has returned

原因是多调用了一个wg.Add(1)

4、双检查实现单例

package doublecheck
import (
"sync"
)
type Once struct {
m sync.Mutex
done uint32
}
func (o *Once) Do(f func()) {
if o.done == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
o.done = 1
f()
}
}

这一题争议最大,博主是想告诉我们类似于Java里面缓存变量的问题,我觉得这里是不会出现问题的,因为对象是指针调用;运行本身没有问题,但是会出现数据竞争。使用go run -race main.go运行就会发现有数据竞争。

5、同步对象使用后不能被拷贝

package main
import (
"fmt"
"sync"
)
type MyMutex struct {
count int
sync.Mutex
}
func main() {
var mu MyMutex
mu.Lock()
var mu2 = mu
mu.count++
mu.Unlock()
mu2.Lock()
mu2.count++
mu2.Unlock()
fmt.Println(mu.count, mu2.count)
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!

这个原因就是同步对象使用过之后不能再被拷贝,如果上面把mu.Lock()var mu2 = mu这两行进行交换一下就可以了。 使用过后不可以复制的对象有:


// A Cond must not be copied after first use.
type Cond struct
// A Map must not be copied after first use.
type Map struct
// A Mutex must not be copied after first use.
type Mutex struct
// A Pool must not be copied after first use.
type Pool struct
// A RWMutex must not be copied after first use.
type RWMutex struct
// A WaitGroup must not be copied after first use.
type WaitGroup struct

sync包下的struct除了Once这个结构体其他的使用过后都不能被复制。不能被复制也包括函数传递参数,比如如下的使用是错误的:

func main{
var wa sync.WaitGroup
for i:=0 ;i < 10 ;i++ {
wa.Add(1)
go func(wa sync.WaitGroup){
fmt.Println("wa.Down()")
wa.Down()
}(wa)
}
wa.Wait()
}

上面的代码就有问题,首先wa对象以及使用了wa.Add(1),后面开启一个go时确做参数传入,此时传入的是一个副本,就会出现不能正确的执行wa.Down();此处可以修改为传递指针go func(wa *sync.WaitGroup)或者使用闭包的方式使用WaitGroup对象。

7、channel

package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var ch chan int
go func() {
ch = make(chan int, 1)
ch <- 1
}()
go func(ch chan int) {
time.Sleep(time.Second)
<-ch
}(ch)
c := time.Tick(1 * time.Second)
for range c {
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
}
}
$ go run main.go
#goroutines: 2
#goroutines: 2
#goroutines: 2

这一题比较简单,但是我还是忽略了一个关键点,最后运行中有两个g,原因就是time.Tick其实是开启了一个G来计时的,然后通过Channel来通知。

我们来看一下time.Tick里面的实现:

time 包
func NewTicker(d Duration) *Ticker{
...
startTimer(&t.r)// 这个方法对应到runtime.startTimer()方法上
}
runtime 包
// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
if raceenabled {
racerelease(unsafe.Pointer(t))
}
addtimer(t)
}
//在addtimer()方法中启动了一个`G`。

13、for range 问题

package main
import (
"fmt"
"sync"
"time"
)
type T struct {
V int
}
func (t *T) Incr(wg *sync.WaitGroup) {
t.V++
wg.Done()
}
func (t *T) Print() {
time.Sleep(1e9)
fmt.Print(t.V)
}
func main() {
var wg sync.WaitGroup
wg.Add(10)
var ts = make([]T, 10)
for i := 0; i < 10; i++ {
ts[i] = T{i}
}
for _, t := range ts {
go t.Incr(&wg)
}
wg.Wait()
for _, t := range ts {
go t.Print()
}
time.Sleep(5 * time.Second)
}
$ go run main.go
999999999

这一题可以是因为使用了for range的方式,这种方式中的t只是一个变量,会一直在边,当使用go t.Incr()的时候,此时的t已经变为了最后一个值,所以输出都是9,这个在使用map时也有这个问题,得到 的k/v只是值的一个拷贝。